52. 数据框拼接

本章概要

  • 学习材料:带来源键、日期、证券代码与指标的多表数据。
  • 本章任务:运行 lst-concat-basiclst-merge-basiclst-merge-multikey,完成纵向和多键连接。
  • 完成后你将得到:来源追踪表、连接指示列、匹配/未匹配与行数核对。
  • 自我检查:核对多键唯一和连接前后行数;多对多爆炸或键类型不同时先检查原因。
  • 拓展练习:把连接方法应用到另一组财务/行情表。

为什么需要数据框拼接?

在真实的数据分析项目中,数据往往分散在多个来源中:

  • 行情数据:来自交易所的实时价格、成交量
  • 财务数据:来自公司财报的资产负债表、利润表
  • 宏观数据:来自统计部门的 GDP、CPI、利率
  • 情绪数据:来自新闻舆情、社交媒体的投资者情绪

将不同来源的数据整合在一起,是日常数据分析的核心挑战。

数据整合面临的核心问题

问题 说明
粒度不匹配 日频行情 vs 季频财务
时间对齐 不同市场的交易日历不同
键值识别 如何正确匹配同一公司的不同数据源?
重复数据 同一指标来自多个提供商
性能瓶颈 大规模数据集的合并操作极其耗时

本章学习目标

  • 理解 concatmergejoin 的本质区别与应用场景
  • 掌握不同类型连接(inner / left / right / outer)的含义
  • 学习金融时间序列数据的对齐与聚合技术
  • 了解大数据环境下的性能优化策略

数据拼接的两种方向

垂直拼接与水平拼接对比 左侧展示垂直拼接(行向堆叠),右侧展示水平拼接(列向合并) 垂直拼接(行向堆叠) D₁ (n 行) D₂ (m 行) 结果 (n+m 行) 水平拼接(列向合并) D₁ D₂ 结果(列数增加)

垂直拼接的数学表达

设两个数据矩阵 \(D_1\)\(n\) 行)和 \(D_2\)\(m\) 行),垂直拼接结果为:

\[ \large D_{\text{concat}} = \begin{bmatrix} D_1 \\ D_2 \end{bmatrix} \]

前提条件:两个数据集必须具有相同的列结构(相同列名和数据类型)。

水平拼接的数学表达

将多个数据集沿列方向(横向)合并,增加变量数量:

\[ \large D_{\text{merge}} = [D_1 \mid D_2] \]

前提条件:两个数据集必须具有相同的行数或可通过键值对齐。

关系代数基础

Pandas 的 merge 操作基于关系代数中的连接(Join)运算:

\[ \large R \bowtie_{\theta} S = \{ (r, s) \in R \times S \mid \theta(r, s) \} \]

  • \(R, S\):两个关系(数据表)
  • \(\bowtie\):连接运算符
  • \(\theta\):连接条件(通常是键值相等)

运行前预测|平台任务1:从 Excel 导入两个 Sheet 的数据

  • 输入预测:运行前先写出 price_JantoMarprice_AprtoJui 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到price_AprtoJui 的结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务1:从 Excel 导入两个 Sheet 的数据”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务1:从 Excel 导入两个 Sheet 的数据

展开完整代码(投影默认折叠)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库

price_JantoMar = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet1",header=0,index_col=0)#从外部导入Sheet1的5只股票信息链接为https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx

print(price_JantoMar.head()) #查看前五行数据

print(price_JantoMar.tail()) #查看后五行数据

price_AprtoJui = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet2",header=0,index_col=0)##从外部导入Sheet2的5只股票信息链接为https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx

print(price_AprtoJui.head()) #查看前五行数据

print(price_AprtoJui.tail()) #查看后五行数据

任务复盘|平台任务1:从 Excel 导入两个 Sheet 的数据

运行后核对:核对 price_JantoMarprice_AprtoJui 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:把输入表替换为本地中国上市公司数据的同结构子集;指出必须保持的字段、数据类型和质量检查。

运行前预测|平台任务2:使用 concat 按行拼接

  • 输入预测:运行前先写出 price_JantoMarprice_AprtoJuiprice_JantoJul 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到price_JantoJul 的结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务2:使用 concat 按行拼接”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务2:使用 concat 按行拼接

展开完整代码(投影默认折叠)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库

# 从Excel文件读取数据存入price_JantoMar
price_JantoMar = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet1",header=0,index_col=0)

# 从Excel文件读取数据存入price_AprtoJui
price_AprtoJui = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet2",header=0,index_col=0)

price_JantoJul = pd.concat([price_JantoMar,price_AprtoJui],axis=0) #使用concat函数按行拼接

print(price_JantoJul.head())  #前五行数据

print(price_JantoJul.tail())  #后五行数据

任务复盘|平台任务2:使用 concat 按行拼接

运行后核对:核对 price_JantoMarprice_AprtoJuiprice_JantoJul 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:把输入表替换为本地中国上市公司数据的同结构子集;指出必须保持的字段、数据类型和质量检查。

运行前预测|平台任务3:导入不同 Sheet 的股票数据

  • 输入预测:运行前先写出 price_3stocksprice_2stocks 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到price_2stocks 的结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务3:导入不同 Sheet 的股票数据”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务3:导入不同 Sheet 的股票数据

展开完整代码(投影默认折叠)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库

price_3stocks = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet1",header=0,index_col=0) #导入数据Sheet1 链接https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx

print(price_3stocks.head()) #查看前五行数据

print(price_3stocks.tail()) #查看后五行数据

price_2stocks = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet2",header=0,index_col=0) #导入数据Sheet1 链接https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx

print(price_2stocks.head()) #查看前五行数据

print(price_2stocks.tail()) #查看后五行数据 

任务复盘|平台任务3:导入不同 Sheet 的股票数据

运行后核对:核对 price_3stocksprice_2stocks 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:把输入表替换为本地中国上市公司数据的同结构子集;指出必须保持的字段、数据类型和质量检查。

运行前预测|平台任务4:三种按列拼接方法对比

  • 输入预测:运行前先写出 price_3stocksprice_2stocksprice_5stocks_concatprice_5stocks_merge 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到price_5stocks_join 的结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务4:三种按列拼接方法对比”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务4:三种按列拼接方法对比

展开完整代码(投影默认折叠)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库

# 从Excel文件读取数据存入price_3stocks
price_3stocks = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet1",header=0,index_col=0)

# 从Excel文件读取数据存入price_2stocks
price_2stocks = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet2",header=0,index_col=0)

price_5stocks_concat = pd.concat([price_3stocks,price_2stocks],axis=1) #使用concat函数按列拼接

print(price_5stocks_concat.head()) #查看前五行数据

price_5stocks_merge = pd.merge(left=price_3stocks,right=price_2stocks,left_index=True,right_index=True) #使用merge函数按列拼接

print(price_5stocks_merge.head()) #查看前五行数据

price_5stocks_join = price_3stocks.join(price_2stocks,on="日期") #用join函数按列拼接

print(price_5stocks_join.head()) #查看前五行数据

任务复盘|平台任务4:三种按列拼接方法对比

运行后核对:核对 price_3stocksprice_2stocksprice_5stocks_concatprice_5stocks_merge 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:把输入表替换为本地中国上市公司数据的同结构子集;指出必须保持的字段、数据类型和质量检查。

concat 函数:垂直拼接的首选工具

pd.concat() 是 Pandas 中最通用的拼接函数,核心语法:

pd.concat([df1, df2], axis=0, ignore_index=True)
  • axis=0:垂直拼接(行方向堆叠)
  • axis=1:水平拼接(列方向合并)
  • ignore_index=True:重置索引,避免重复

concat 关键参数一览

参数 作用 默认值
objs 要拼接的对象列表 必需
axis 拼接方向(0=行,1=列) 0
ignore_index 忽略原索引,重新生成 False
keys 创建多级索引标识来源 None
join 列对齐方式(inner/outer) outer
sort 是否对列排序 True

示例:concat 垂直拼接

Listing 1: 使用concat进行垂直拼接
展开完整代码
import pandas as pd
import numpy as np

# 贵州茅台(600519.SH)收益率数据
stock_a_returns = pd.DataFrame({
    '日期': pd.date_range('2024-01-01', periods=3),
    '股票代码': ['600519.SH'] * 3,
    '收益率': [0.02, -0.01, 0.03]
})

# 五粮液(000858.SZ)收益率数据
stock_b_returns = pd.DataFrame({
    '日期': pd.date_range('2024-01-04', periods=3),
    '股票代码': ['000858.SZ'] * 3,
    '收益率': [0.01, 0.02, -0.02]
})

# 垂直拼接,ignore_index=True 重置索引
all_returns = pd.concat(
    [stock_a_returns, stock_b_returns], ignore_index=True
)

print('股票A数据:')
print(stock_a_returns)
print('\n股票B数据:')
print(stock_b_returns)
print('\n拼接结果:')
print(all_returns)
股票A数据:
          日期       股票代码   收益率
0 2024-01-01  600519.SH  0.02
1 2024-01-02  600519.SH -0.01
2 2024-01-03  600519.SH  0.03

股票B数据:
          日期       股票代码   收益率
0 2024-01-04  000858.SZ  0.01
1 2024-01-05  000858.SZ  0.02
2 2024-01-06  000858.SZ -0.02

拼接结果:
          日期       股票代码   收益率
0 2024-01-01  600519.SH  0.02
1 2024-01-02  600519.SH -0.01
2 2024-01-03  600519.SH  0.03
3 2024-01-04  000858.SZ  0.01
4 2024-01-05  000858.SZ  0.02
5 2024-01-06  000858.SZ -0.02

concat 多级索引:追溯数据来源

Listing 2: 使用concat创建多级索引
展开实现代码
# keys 参数为每个数据框分配标识键,创建多级索引
multi_index_returns = pd.concat(
    [stock_a_returns, stock_b_returns],
    keys=['股票A', '股票B'],
    names=['数据源', '行号']
)

print('多级索引结构:')
print(multi_index_returns.head(4))

print('\n索引信息:')
print(multi_index_returns.index.names)

# 选择特定来源的数据
print('\n仅选择股票A的数据:')
print(multi_index_returns.loc['股票A'].head(1))
多级索引结构:
               日期       股票代码   收益率
数据源 行号                            
股票A 0  2024-01-01  600519.SH  0.02
    1  2024-01-02  600519.SH -0.01
    2  2024-01-03  600519.SH  0.03
股票B 0  2024-01-04  000858.SZ  0.01

索引信息:
['数据源', '行号']

仅选择股票A的数据:
           日期       股票代码   收益率
行号                            
0  2024-01-01  600519.SH  0.02

concat 多级索引:使用决策

  • 适用:多源质量检查,需要保留数据来源标识
  • 检索:用第一层索引定位单一来源的观测

merge 函数:基于键值的水平合并

pd.merge() 基于共同的键值列进行水平合并:

pd.merge(left, right, on='键值列', how='inner')
  • on:指定匹配的键值列
  • how:连接方式(inner / left / right / outer)

示例:merge 内连接

Listing 3: 使用merge进行基础连接
展开完整代码
# 股票基本信息
stock_info = pd.DataFrame({
    '股票代码': ['600519.SH', '000858.SZ', '600036.SH'],
    '股票名称': ['贵州茅台', '五粮液', '招商银行'],
    '行业': ['食品饮料', '食品饮料', '金融']
})

# 股票财务数据(注意:第3只是中国平安,不是招商银行)
financial_data = pd.DataFrame({
    '股票代码': ['600519.SH', '000858.SZ', '601318.SH'],
    'PE': [35.2, 25.8, 10.5],
    'PB': [12.5, 8.3, 1.2]
})

# 内连接:只保留两边都有的股票
inner_result = pd.merge(
    stock_info, financial_data, on='股票代码', how='inner'
)

print('股票基本信息:')
print(stock_info)
print('\n财务数据:')
print(financial_data)
print('\n内连接结果(只保留两边都有的股票):')
print(inner_result)
股票基本信息:
        股票代码  股票名称    行业
0  600519.SH  贵州茅台  食品饮料
1  000858.SZ   五粮液  食品饮料
2  600036.SH  招商银行    金融

财务数据:
        股票代码    PE    PB
0  600519.SH  35.2  12.5
1  000858.SZ  25.8   8.3
2  601318.SH  10.5   1.2

内连接结果(只保留两边都有的股票):
        股票代码  股票名称    行业    PE    PB
0  600519.SH  贵州茅台  食品饮料  35.2  12.5
1  000858.SZ   五粮液  食品饮料  25.8   8.3

内连接的数学含义

\[ \large R \bowtie S = \{ (r, s) \mid r[\text{key}] = s[\text{key}] \} \]

只有键值在两个数据集中都存在的行才会被保留。

  • 贵州茅台 ✅ 两边都有 → 保留
  • 五粮液 ✅ 两边都有 → 保留
  • 招商银行 ❌ 只在左表 → 丢弃
  • 中国平安 ❌ 只在右表 → 丢弃

四种连接类型对比

Listing 4: 四种连接类型的对比
展开实现代码
# 左连接:保留左表所有行
left_result = pd.merge(
    stock_info, financial_data, on='股票代码', how='left'
)

# 右连接:保留右表所有行
right_result = pd.merge(
    stock_info, financial_data, on='股票代码', how='right'
)

# 外连接:保留所有行
outer_result = pd.merge(
    stock_info, financial_data, on='股票代码', how='outer'
)

print('左连接结果(保留左边所有股票):')
print(left_result)
print('\n右连接结果(保留右边所有股票):')
print(right_result)
print('\n外连接结果(保留所有股票):')
print(outer_result)
左连接结果(保留左边所有股票):
        股票代码  股票名称    行业    PE    PB
0  600519.SH  贵州茅台  食品饮料  35.2  12.5
1  000858.SZ   五粮液  食品饮料  25.8   8.3
2  600036.SH  招商银行    金融   NaN   NaN

右连接结果(保留右边所有股票):
        股票代码  股票名称    行业    PE    PB
0  600519.SH  贵州茅台  食品饮料  35.2  12.5
1  000858.SZ   五粮液  食品饮料  25.8   8.3
2  601318.SH   NaN   NaN  10.5   1.2

外连接结果(保留所有股票):
        股票代码  股票名称    行业    PE    PB
0  000858.SZ   五粮液  食品饮料  25.8   8.3
1  600036.SH  招商银行    金融   NaN   NaN
2  600519.SH  贵州茅台  食品饮料  35.2  12.5
3  601318.SH   NaN   NaN  10.5   1.2

连接类型的决策指南

场景 推荐类型 理由
在基础信息表中匹配补充信息 left 保证主要数据表中的记录不丢失
数据源可靠性相同 inner 只保留两边都有的高质量数据
整合多个不完整来源 outer 最大化信息利用

三问法:先确定业务全集,再决定保留左表、交集或并集;连接后必须核对键唯一性与行数。

join 方法:基于索引的便捷合并

df.join()merge 的特例,专门用于基于索引的合并:

df1.join(df2, how='left')
  • 使用 join:数据已经以键值为索引,代码更简洁
  • 使用 merge:需要基于列进行连接,或需要更复杂的连接条件

示例:join 基于索引合并

Listing 5: 使用join基于索引合并
展开实现代码
# 收益率数据,股票代码为索引
returns = pd.DataFrame({
    '日收益率': [0.02, 0.01, -0.01]
}, index=['600519.SH', '000858.SZ', '600036.SH'])

# 波动率数据,股票代码为索引(第3只不同)
volatility = pd.DataFrame({
    '年化波动率': [0.25, 0.30, 0.20]
}, index=['600519.SH', '000858.SZ', '601318.SH'])

# 基于索引左连接
joined_data = returns.join(volatility, how='left')

print('收益率数据:')
print(returns)
print('\n波动率数据:')
print(volatility)
print('\n基于索引的左连接结果:')
print(joined_data)
收益率数据:
           日收益率
600519.SH  0.02
000858.SZ  0.01
600036.SH -0.01

波动率数据:
           年化波动率
600519.SH   0.25
000858.SZ   0.30
601318.SH   0.20

基于索引的左连接结果:
           日收益率  年化波动率
600519.SH  0.02   0.25
000858.SZ  0.01   0.30
600036.SH -0.01    NaN

三种拼接方法总结对比

方法 适用场景 拼接方向 匹配方式
pd.concat() 结构相同的数据堆叠 纵向/横向 索引对齐
pd.merge() 基于键值列的关联 横向 列值匹配
df.join() 基于索引的关联 横向 索引匹配

金融实战:多源数据整合

任务:整合股票基本信息、行情数据、财务指标,构建完整的分析数据集。

Listing 6: 金融多源数据整合实战
展开完整代码
import pandas as pd

# 数据源1:股票基本信息(主表)
stock_basic = pd.DataFrame({
    '股票代码': ['600519.SH', '000858.SZ', '600036.SH', '601318.SH'],
    '股票名称': ['贵州茅台', '五粮液', '招商银行', '中国平安'],
    '上市日期': ['2001-08-27', '1998-04-27', '2002-04-09', '2007-03-01'],
    '行业': ['食品饮料', '食品饮料', '金融', '金融']
})

# 数据源2:日行情数据(缺少中国平安)
daily_quote = pd.DataFrame({
    '股票代码': ['600519.SH', '000858.SZ', '600036.SH'],
    '收盘价': [1850.00, 158.50, 32.80],
    '涨跌幅': [1.5, -0.8, 0.5]
})

# 数据源3:财务指标(缺少招商银行)
financial_metrics = pd.DataFrame({
    '股票代码': ['600519.SH', '000858.SZ', '601318.SH'],
    'ROE': [25.8, 22.3, 15.6],
    '负债率': [18.5, 30.2, 92.5]
})

金融实战:逐步整合数据

Listing 7: 逐步左连接整合数据
展开实现代码
# 步骤1:基本信息 + 行情数据(左连接)
step1 = pd.merge(
    stock_basic, daily_quote,
    on='股票代码', how='left', indicator=True
)
print('步骤1:基本信息 + 行情数据')
print(step1)

# 步骤2:继续加入财务指标
final_data = pd.merge(
    step1, financial_metrics,
    on='股票代码', how='left', suffixes=('', '_财务')
)
print('\n最终整合结果:')
print(final_data)

# 数据完整性分析
print('\n缺失值统计:')
print(final_data.isna().sum())
步骤1:基本信息 + 行情数据
        股票代码  股票名称        上市日期    行业     收盘价  涨跌幅     _merge
0  600519.SH  贵州茅台  2001-08-27  食品饮料  1850.0  1.5       both
1  000858.SZ   五粮液  1998-04-27  食品饮料   158.5 -0.8       both
2  600036.SH  招商银行  2002-04-09    金融    32.8  0.5       both
3  601318.SH  中国平安  2007-03-01    金融     NaN  NaN  left_only

最终整合结果:
        股票代码  股票名称        上市日期    行业     收盘价  涨跌幅     _merge   ROE   负债率
0  600519.SH  贵州茅台  2001-08-27  食品饮料  1850.0  1.5       both  25.8  18.5
1  000858.SZ   五粮液  1998-04-27  食品饮料   158.5 -0.8       both  22.3  30.2
2  600036.SH  招商银行  2002-04-09    金融    32.8  0.5       both   NaN   NaN
3  601318.SH  中国平安  2007-03-01    金融     NaN  NaN  left_only  15.6  92.5

缺失值统计:
股票代码      0
股票名称      0
上市日期      0
行业        0
收盘价       1
涨跌幅       1
_merge    0
ROE       1
负债率       1
dtype: int64

数据整合的关键决策

  • 主表选择:以股票基本信息为主表,使用 left join 确保不丢失股票
  • 来源追踪:使用 indicator=True 标识每条数据是否成功匹配
  • 列名冲突:使用 suffixes 参数处理重名列
  • 缺失值含义:财务指标缺失可能意味着该股票尚未发布财报

性能优化:大数据集合并

Listing 8: 大数据集合并的性能优化
展开完整参考实现
import pandas as pd  # 导入Pandas以构造并连接确定性课堂示例
import numpy as np  # 导入NumPy以重复证券键与构造数值列
from time import perf_counter  # 导入高分辨率计时器以记录当前环境结果

company_count = 200  # 设置轻量课堂公司数以避免渲染占用过量内存
date_count = 100  # 设置每家公司日期数以形成可重复连接规模
company_codes = [f'{company_index:06d}.SH' for company_index in range(company_count)]  # 生成无真实公司含义的中性代码
quotes = pd.DataFrame({'股票代码':np.repeat(company_codes,date_count),'日期':list(pd.date_range('2020-01-01',periods=date_count))*company_count,'收盘价':10+np.arange(company_count*date_count)%90})  # 构造确定性课堂行情示例数据
financials = pd.DataFrame({'股票代码':company_codes,'ROE':5+np.arange(company_count)%25,'市值':50+np.arange(company_count)*5})  # 构造确定性课堂财务示例数据
Listing 9
展开当前环境基准与等价性核对
plain_start = perf_counter()  # 记录普通字符串键连接起点
plain_result = pd.merge(quotes,financials,on='股票代码')  # 执行普通字符串键连接
plain_seconds = perf_counter()-plain_start  # 记录当前运行环境普通连接耗时
category_quotes = quotes.assign(股票代码=quotes['股票代码'].astype('category'))  # 将左键转换为分类类型
category_financials = financials.assign(股票代码=financials['股票代码'].astype('category'))  # 将右键转换为分类类型
category_start = perf_counter()  # 记录分类键连接起点
category_result = pd.merge(category_quotes,category_financials,on='股票代码')  # 执行分类键连接
category_seconds = perf_counter()-category_start  # 记录当前运行环境分类连接耗时
assert plain_result.shape==category_result.shape  # 核对两种实现输出形状一致
assert plain_result['ROE'].sum()==category_result['ROE'].sum()  # 核对连接后业务总量一致
print({'rows':len(plain_result),'plain_seconds':plain_seconds,'category_seconds':category_seconds,'policy':'仅报告本机本次结果,不外推固定倍数'})  # 输出可核对基准并限定解释
{'rows': 20000, 'plain_seconds': 0.005761166103184223, 'category_seconds': 0.002320139668881893, 'policy': '仅报告本机本次结果,不外推固定倍数'}

性能优化清单

优化策略 说明
键值类型优化 将字符串键值转换为 category 类型
索引优化 对键值列建立索引(df.set_index()
避免重复 合并前检查并删除重复数据
分块处理 超大文件考虑分块读取和合并
使用 Dask 超出内存容量时使用并行计算框架

高级主题:多键连接

Listing 10: 基于多个键值进行连接
展开实现代码
# 行情数据
quotes = pd.DataFrame({
    '股票代码': ['600519.SH', '600519.SH', '000858.SZ'],
    '日期': ['2024-01-01', '2024-01-02', '2024-01-01'],
    '收盘价': [1850.0, 1870.0, 158.5]
})

# 财务数据
financals = pd.DataFrame({
    '股票代码': ['600519.SH', '600519.SH', '000858.SZ'],
    '日期': ['2024-01-01', '2024-01-02', '2024-01-01'],
    'PE': [35.2, 35.8, 25.8]
})

# 基于股票代码 + 日期两个键进行合并
merged = pd.merge(
    quotes, financals,
    on=['股票代码', '日期'], how='inner'
)

print('多键连接结果:')
print(merged)
多键连接结果:
        股票代码          日期     收盘价    PE
0  600519.SH  2024-01-01  1850.0  35.2
1  600519.SH  2024-01-02  1870.0  35.8
2  000858.SZ  2024-01-01   158.5  25.8

多键连接的数学含义

\[ \large R \bowtie_{k_1, k_2} S = \{ (r, s) \mid r[k_1] = s[k_1] \land r[k_2] = s[k_2] \} \]

只有所有指定的键值都匹配时,两行数据才会被连接。

应用场景:确保分析的是同一股票在同一天的完整数据,避免错误地将不同日期的数据拼接在一起。

本章总结

  • pd.concat()堆叠数据,适合结构相同的数据框沿行/列方向拼接
  • pd.merge()关联数据,基于键值列进行水平合并,支持四种连接类型
  • df.join()索引合并,merge 的简化版,适合索引已设好的场景
  • 连接类型选择:根据业务需求决定使用 inner / left / right / outer
  • 性能优化:键值转为 category 类型可显著提升大数据集合并速度

随堂练习

  • 问题 1|需要准备哪些数据?:带来源键、日期、证券代码与指标的多表数据。
  • 问题 2|需要完成哪些操作?:运行 lst-concat-basiclst-merge-basiclst-merge-multikey,完成纵向和多键连接。
  • 问题 3|应得到哪些结果?:来源追踪表、连接指示列、匹配/未匹配与行数核对。
  • 问题 4|怎样确认结果可靠?:核对多键唯一和连接前后行数;多对多爆炸或键类型不同时先检查原因。
  • 问题 5|换一个情境,怎样继续应用?:把连接方法应用到另一组财务/行情表。
  • 作答提示:请依次写清所用数据、分析过程、所得结果、核对方法和拓展思考。课程所需数据见前言中的下载入口;教学平台固定题按页面说明完成。

教师参考解答|答案与说明 1

  • 所用数据与字段:财务表与基础信息表通过 code,date 连接;右表中每组代码和日期至多一行。

教师参考解答|代码 1

展开代码(代码区可独立滚动)
import pandas as pd  # 导入表格工具以构造、对齐和核对数据
left=pd.DataFrame({'code':['A','A','B'],'date':['2024Q1','2024Q2','2024Q1'],'revenue':[10,12,8]})  # 建立待连接的左侧收入表
right=pd.DataFrame({'code':['A','A','B'],'date':['2024Q1','2024Q2','2024Q1'],'area':['上海','上海','浙江']})  # 建立待连接的右侧地区表
joined=left.merge(right,on=['code','date'],how='left',validate='one_to_one',indicator=True)  # 建立待连接的左侧收入表
assert len(joined)==len(left) and joined['_merge'].eq('both').all()  # 建立待连接的左侧收入表
assert joined['revenue'].sum()==left['revenue'].sum()  # 建立待连接的左侧收入表
print(joined,joined['_merge'].value_counts())  # 按公司与期间键执行一对一连接

教师参考解答|答案与说明 2

  • 解释答案:concat 按轴堆叠同类表,merge 按业务键连接异类字段;一对一验证、连接指示列和行数/金额守恒共同防止重复扩张。
  • 拓展应用答案:拓展应用到订单与门店表时,键改为门店号+日期,使用 validate 和 indicator,并核对连接前后订单数与销售额总和。
  • 参考结果:3 行匹配表与 both=3另行核对:核对左表行数、营收总和和键唯一性;故意复制右表中的连接键时,validate 应当报错。
  • 常见错误:只按公司不按期;多对多膨胀;连接后不核对未匹配行。